
package com.studiofortress.sf.graphics.display;

import com.studiofortress.sf.graphics.GraphicsCanvas;
import com.studiofortress.sf.util.collections.ObjectStack;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import javax.media.opengl.GL;
import javax.media.opengl.GLCanvas;

/**
 * This is a GraphicsDisplay that will wrap the given Container. The given
 * container will hold the canvas for OpenGL graphics. It should be expected
 * that the given Container should not be used by other components whilst this
 * GraphicsWrapper is using it.
 *
 * @author Joseph Lenton
 * @param <C> The class of Container this will wrap.
 */
public class GraphicsWrapper<C extends Container> implements GraphicsDisplay
{
    private final MouseMotionAdapter controlMotionEventHandler;
    private final MouseAdapter controlClickEventHandler;
    private final KeyAdapter controlKeyEventHandler;

    private final C innerComponent;
    private final GLCanvas canvas;

    private final ObjectStack<ControlEvent> events;

    // control interface
    private DisplayControls controls;

    /**
     * This class is not meant to be implimented,
     * and is simply to create the workings for all
     * of the various types of Graphic Windows.
     */
    public GraphicsWrapper(C innerComponent, int width, int height)
    {
        // create the inner canvas
        canvas = new GraphicsCanvas(width, height);
        this.innerComponent = innerComponent;
        this.innerComponent.add(canvas);

        this.innerComponent.setIgnoreRepaint(true);
        
        controls = createControls();
        events = new ObjectStack<ControlEvent>();
        
        // create the control handler adapters
        controlMotionEventHandler = new MouseMotionAdapter() {
            private int lastX = getWidth()/2;
            private int lastY = getHeight()/2;

            @Override
            public void mouseMoved(MouseEvent e)
            {
                final int x = e.getX();
                final int y = e.getY();

                events.push( new ControlMotionEvent(x-lastX, y-lastY) );

                lastX = x;
                lastY = y;
            }
        };
        controlClickEventHandler = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e)
            {
                final int mouseButton;

                if (e.getButton() == MouseEvent.BUTTON1) {
                    mouseButton = Controls.MOUSE_LEFT;
                } else if (e.getButton() == MouseEvent.BUTTON3) {
                    mouseButton = Controls.MOUSE_RIGHT;
                } else if (e.getButton() == MouseEvent.BUTTON2) {
                    mouseButton = Controls.MOUSE_MIDDLE;
                } else {
                    return;
                }

                events.push( new ControlClickEvent(mouseButton) );
            }
        };
        controlKeyEventHandler = new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e)
            {
                // if it's a character
                events.push( new ControlKeyCharEvent(e.getKeyChar()) );
            }
            
            @Override
            public void keyPressed(KeyEvent e)
            {
                // it's a non character, i.e. a backspace
                events.push( new ControlKeyEvent(e.getKeyCode(), true) );
            }
            
            @Override
            public void keyReleased(KeyEvent e)
            {
                events.push( new ControlKeyEvent(e.getKeyCode(), false) );
            }
        };
    }
    
    DisplayControls createControls()
    {
        return new DisplayControls(this);
    }

    /**
     * Returns the OpenGL graphics device for the window.
     */
    @Override
    public GL getGL()
    {
        return getCanvas().getGL();
    }

    @Override
    public GLCanvas getCanvas()
    {
        return canvas;
    }

    /**
     * This is a means to get under the hood of this GraphicsWrapper and into
     * the underlying Swing component. This should only be used for dirty hacks.
     * @return The swing container that this GraphicsWrapper wraps.
     */
    public C getContainer()
    {
        return innerComponent;
    }

    /**
     * Makes the mouse pointer invisible, hiding it.
     */
    @Override
    public void hideMouse()
    {
        final BufferedImage icon = new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB);
        final Point mousePoint = new Point(0,0);
        getContainer().setCursor(
                getContainer().getToolkit().createCustomCursor(
                icon, mousePoint, "null"));
    }

    /**
     * Makes the mouse pointer visible.
     */
    @Override
    public void showMouse()
    {
        getContainer().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
    }

    /**
     * @return the width of the graphics area of the window.
     */
    @Override
    public int getWidth()
    {
        return getCanvas().getWidth();
    }

    /**
     * @return the height of the graphics area of the window.
     */
    @Override
    public int getHeight()
    {
        return getCanvas().getHeight();
    }

    /**
     * For retireving the Control object for this window.
     * This object allows you to detect any interaction
     * between the user and the window.
     * @return The control device for this window.
     */
    @Override
    public Controls getControls()
    {
        return controls;
    }

    /**
     * Will repaint the window, displaying anything that has been drawn to it.
     * Typically this will be called at the end of all drawing.
     */
    @Override
    public final void paint()
    {
        getCanvas().display();
        getCanvas().swapBuffers();
    }

    /**
     * Tells you if the Graphics Window
     * is still running or not.
     * A window is not running if it has been closed.
     * @return True if the window is still running, false if not.
     */
    @Override
    public boolean isRunning()
    {
        return getContainer().isVisible();
    }

    @Override
    public void enableControls(ControlEventType... types)
    {
        final GLCanvas canvas = getCanvas();

        /* It both add and remove listeners to ensure they are only in there
         * once. */
        for ( int i = 0; i < types.length; i++ ) {
            final ControlEventType type = types[i];

            if ( type == ControlEventType.MOTION ) {
                canvas.removeMouseMotionListener( controlMotionEventHandler );
                canvas.addMouseMotionListener( controlMotionEventHandler );
            } else if ( type == ControlEventType.CLICK ) {
                canvas.removeMouseListener( controlClickEventHandler );
                canvas.addMouseListener( controlClickEventHandler );
            } else if ( type == ControlEventType.KEY ) {
                canvas.removeKeyListener( controlKeyEventHandler );
                canvas.addKeyListener( controlKeyEventHandler );
            }
        }
    }

    @Override
    public void disableControls(ControlEventType... types)
    {
        final GLCanvas canvas = getCanvas();
        
        /* It both add and remove listeners to ensure they are only in there
         * once. */
        for ( int i = 0; i < types.length; i++ ) {
            final ControlEventType type = types[i];

            if ( type == ControlEventType.MOTION ) {
                canvas.removeMouseMotionListener( controlMotionEventHandler );
            } else if ( type == ControlEventType.CLICK ) {
                canvas.removeMouseListener( controlClickEventHandler );
            } else if ( type == ControlEventType.KEY ) {
                canvas.removeKeyListener( controlKeyEventHandler );
            }
        }
    }

    @Override
    public synchronized void updateControls(ObjectStack<ControlEvent> events)
    {
        controls.update();
        
        /* Don't worry about it reversing the events,
         * they are in reverse order already.
         * Reversing again puts the oldest events on top,
         * so they will be processed first. */
        while ( this.events.size() > 0 ) {
            events.push( this.events.pop() );
        }
    }
}